package sn;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import sn.functions.ActivationFunction;
import sn.layers.HiddenLayer;
import sn.layers.InputLayer;
import sn.layers.Layer;
import sn.layers.OutputLayer;

import com.google.common.collect.Lists;

public class Net {
	
	private InputLayer inputLayer;
	private List<HiddenLayer> layers;
	private OutputLayer outputLayer;
	
	public List<Double> compute(Double ... inputs){
		if(inputs.length != inputLayer.getNeuronCount())
			throw new RuntimeException("Size of input must be equal to size of input layer.");
		return inputLayer.compute(Arrays.asList(inputs));
	}
	
	/**
	 * 
	 * One of ways to create net:
	 * 1. Use this to create input and output layers.
	 * 2. Use addHiddenLayer as many times as you want.
	 * 3. Use compile() before start working with net.
	 * 
	 * @param inputNeurons
	 * @param outputNeurons
	 * @param activationFunction output layer activation function
	 */
	public Net(int inputNeurons, int outputNeurons, ActivationFunction activationFunction){
		this.inputLayer = new InputLayer(inputNeurons);
		this.outputLayer = new OutputLayer(outputNeurons, activationFunction);
		this.layers = new ArrayList<HiddenLayer>();
	}
	
	public void addHiddenLayer(int neuronCount, ActivationFunction transferFunction){
		layers.add(new HiddenLayer(neuronCount, transferFunction));
	}
	
	public void addHiddenLayer(HiddenLayer layer){
		layers.add(layer);
	}
	
	/**
	 * 
	 * One of ways to create net:
	 * 1. Use this.
	 * Net is ready, no need to use compile()
	 * 
	 * @param inputNeurons
	 * @param hiddenLayers
	 * @param outputNeurons
	 * @param activationFunction output layer activation function
	 */
	public Net(int inputNeurons, HiddenLayer[] hiddenLayers, int outputNeurons, ActivationFunction activationFunction){
		this(inputNeurons, outputNeurons, activationFunction);
		for(HiddenLayer layer : hiddenLayers){
			addHiddenLayer(layer);
		}
		compile();
	}

	public Net(InputLayer il, List<HiddenLayer> hl, OutputLayer ol) {
		this.inputLayer = il;
		this.outputLayer = ol;
		this.layers = hl;
		compile(false);
	}
	
	public void compile(){
		compile(true);
	}

	private void compile(boolean initializeNeurons) {
		Layer nextLayer = outputLayer;
		for(Layer layer : Lists.reverse(layers)){
			layer.connect(nextLayer, initializeNeurons);
			nextLayer = layer;
		}
		inputLayer.connect(nextLayer, initializeNeurons);
	}
	
	public void train(Double[] input, Double[] output){
		List<Double> networkResult = compute(input);
		outputLayer.train(networkResult, Arrays.asList(output));
	}
	
	@Override
	public String toString(){
		return "Net. Layers: \n" + inputLayer.toString();
	}

}
